﻿/******************************************************************************

    Copyright (c) 2010, Sergey Mudrov
    All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:
        * Redistributions of source code must retain the above copyright
          notice, this list of conditions and the following disclaimer.
        * Redistributions in binary form must reproduce the above copyright
          notice, this list of conditions and the following disclaimer in the
          documentation and/or other materials provided with the distribution.
        * Neither the name of Sergey Mudrov nor the
          names of its contributors may be used to endorse or promote products
          derived from this software without specific prior written permission.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
    ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED. IN NO EVENT SHALL SERGEY MUDROV BE LIABLE FOR ANY
    DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
    ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*******************************************************************************/

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.Generic;
using JeffWilcox.Utilities.Silverlight;
using System.Windows.Browser;
using System.Threading;
using System.IO;
using System.Text;
using System.Runtime.Serialization.Json;
using System.Reflection;
using VkApi.DataTypes;
using System.Windows.Threading;


// Namespace for Vkontakte API.
namespace VkApi
{

    /// <summary>
    /// Class that represents requests composer.
    /// </summary>
    public class RequestsComposer
    {
        /// <summary>
        /// List of Func delegates with long type of result
        /// </summary>
        List<Func<long>> func_list;
        /// <summary>
        /// Current function number
        /// </summary>
        int current_func;
        /// <summary>
        /// Interval between the requests. (450 ms by default)
        /// </summary>
        int interval=450;
        /// <summary>
        /// Dispatcher which is performing the Async function calls
        /// </summary>
        DispatcherTimer timer;

        /// <summary>
        /// Initializes a new instance of the <see cref="RequestsComposer"/> class.
        /// </summary>
        /// <param name="calls_interval">Interval in miliseconds between the requests. (450 ms by default)</param>
        public RequestsComposer(int calls_interval)
        {
            interval = calls_interval;
        }

        /// <summary>
        /// Assigns the list if functions encapsulated by Func.
        /// </summary>
        /// <param name="list">The list.</param>
        public void Compose(List<Func<long>> list)
        {
            func_list = list;
            current_func = 0;
        }

        /// <summary>
        /// Performs the requests.
        /// </summary>
        public void Perform()
        {
            if (func_list != null)
                try
                {
                    if (func_list.Count != 0)
                    {
                        timer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(interval) };
                        timer.Tick += (s, e) =>
                        {
                            if (func_list.Count == current_func)
                                timer.Stop();
                            else
                                func_list[current_func++]();

                        };
                        timer.Start();
                    }
                }
                catch (Exception e) { Trace.WriteLine(e.Message); };

        }

        /// <summary>
        /// Executes the single function from the list.
        /// </summary>
        void FuncExecution()
        {
            func_list[current_func]();
            current_func++;
        }
    }

    /// <summary>
    /// Class that represents Vkontakte API for Silverlight.
    /// </summary>
    public partial class VkApi
    {
        private const string _callbackName = "translator({0})";
        private const string _vkApiUrl = "http://api.vkontakte.ru/api.php?";

        #region Member Variables.

        private readonly object _syncRoot = new object();

        private long _appId = 0;
        private string _secret = null;
        private long _viewerID = 0;
        private bool _isTestMode = false;
        private Parameter[] _defaultParams = null;
        private long _currentEventID = 1;
        private Dictionary<long, CallbackData> _callbacks = new Dictionary<long, CallbackData>();

        /// <summary>
        /// Occurs when [direct request callback].
        /// </summary>
        public event ApiResponse_Handler DirectRequestCallback; 

        #endregion

        /// <summary>
        /// 
        /// </summary>
        internal VkApi(VkScriptable vkScriptable, long appID, string secret, long viewerID)
            : this(vkScriptable, appID, secret, viewerID, false) { }

        /// <summary>
        /// 
        /// </summary>
        internal VkApi(VkScriptable vkScriptable, long appID, string secret, long viewerID, bool isTestMode)
        {
#if DEBUG
            Trace.WriteLine("VkApi: Start init..."); 
#endif

            this._appId = appID;
            this._secret = secret;
            this._viewerID = viewerID;
            this._isTestMode = isTestMode;

            List<Parameter> pars = new List<Parameter>(new Parameter[] { 
                new Parameter("format","json"), 
                new Parameter("v","2.0"), 
                new Parameter("api_id",this._appId.ToString()) 
            });

            if (this._isTestMode)
                pars.Add(new Parameter("test_mode", "1"));

            this._defaultParams = pars.ToArray();

            vkScriptable.ApiResponse += new ApiResponse_Handler(VkScriptable_ApiResponse);

#if DEBUG
            Trace.WriteLine("VkApi: Init succeeded"); 
#endif
        }

        #region Public Interface.

        /// <summary>
        /// Send request to Vkontakte API.
        /// </summary>
        /// <param name="functionName">Method name. See list of methods there: http://vkontakte.ru/page2369282</param>
        /// <param name="parameters">Array of parameters.</param>
        /// <returns>ID of request.</returns>
        public long DirectRequest(string functionName, Parameter[] parameters)
        {
            return this.SendRequest(functionName, new ApiCallback<object>(this.DirectRequestHandler), parameters);
        }
        
        #endregion

        #region Internal implementation.

        /// <summary>
        /// 
        /// </summary>
        private long SendRequest(string functionName, Delegate callback)
        {
            return this.SendRequest(functionName, callback, null);
        }

        /// <summary>
        /// 
        /// </summary>
        private long SendRequest(string functionName, Delegate callback, Parameter[] functionParams)
        {
            return this.SendRequest(functionName, new CallbackData { Callback = callback }, functionParams);
        }

        /// <summary>
        /// 
        /// </summary>
        private long SendRequest(string functionName, CallbackData callbackData, Parameter[] functionParams)
        {
            string line = String.Empty;

            long id = this.GetRequestID();
            this._callbacks.Add(id, callbackData);

            List<Parameter> parsList = new List<Parameter>(this._defaultParams);

            parsList.Add(new Parameter("callback", String.Format(VkApi._callbackName, id)));
            parsList.Add(new Parameter("method", functionName));

            if (functionParams != null && functionParams.Length > 0)
                parsList.AddRange(functionParams);

            parsList.Sort(delegate(Parameter x, Parameter y)
            {
                return String.Compare(x.Name, y.Name);
            });

            // Create line for hashing
            line = this._viewerID.ToString() + this.GetParamsLineForHash(parsList) + this._secret;

            // Create hash
            string hash = MD5CryptoServiceProvider.GetMd5String(line);
            string url = VkApi._vkApiUrl +
                this.GetParamsLineForUri(parsList) + 
                "sig=" + hash.Trim();

            // Create JSON request
            HtmlElement element = HtmlPage.Document.CreateElement("script");
            element.SetProperty("type", "text/javascript");
            element.SetProperty("src", url);

            HtmlPage.Document.DocumentElement.AppendChild(element);

            return id;
        }

        /// <summary>
        /// 
        /// </summary>
        private string GetParamsLineForUri(List<Parameter> parsList)
        {
            StringBuilder sb = new StringBuilder("");

            foreach (Parameter p in parsList)
            {
                sb.Append(p.Name);
                sb.Append("=");
                sb.Append(System.Windows.Browser.HttpUtility.UrlEncode(p.Value.Trim()));
                sb.Append("&");
            }

            return sb.ToString();
        }

        /// <summary>
        /// 
        /// </summary>
        private string GetParamsLineForHash(List<Parameter> parsList)
        {
            try
            {
                StringBuilder sb = new StringBuilder("");

                foreach (Parameter p in parsList)
                {
                    sb.Append(p.Name);
                    sb.Append("=");
                    sb.Append(p.Value.Trim());
                }

                return sb.ToString();
            }
            catch (Exception ex)
            {
#if DEBUG
                Trace.WriteLine(ex.ToString());
#endif
                throw ex;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        private void VkScriptable_ApiResponse(long requestID, string jsonData)
        {
#if DEBUG
            Trace.WriteLine(jsonData);
#endif
            jsonData = System.Windows.Browser.HttpUtility.UrlDecode(jsonData);
            jsonData = jsonData.Replace("{}", "null");

            // Get callback
            Delegate apiCallback = null;
            object response = null;
            try
            {
                // Check if JSON preprocessing needed
                if (this._callbacks[requestID].CallbackJsonPreprocess != null)
                    jsonData = this._callbacks[requestID].CallbackJsonPreprocess(jsonData);

                // Get callback
                apiCallback = this._callbacks[requestID].Callback;
                if (apiCallback == null)
                    return;

                // Get generic arguments
                Type[] callbackArguments = apiCallback.GetType().GetGenericArguments();

                // Transfer JSON data into object
                MethodInfo mi = this.GetType().GetMethod("GetResponse", BindingFlags.Instance | BindingFlags.NonPublic);
                MethodInfo castMethod = mi.MakeGenericMethod(callbackArguments[0]);
                response = castMethod.Invoke(this, new object[] { requestID, jsonData });

                // Invoke callback
                apiCallback.DynamicInvoke(response);
            }
            catch (Exception ex)
            {
#if DEBUG
                Trace.WriteLine("Failed to process API response!" + Environment.NewLine + ex.ToString()); 
#endif

                throw new Exception("Failed to process API response!", ex);
            }
            finally
            {
                // remove callback from dictionary
                if (apiCallback != null)
                    this._callbacks.Remove(requestID);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        private Response<T> GetResponse<T>(long requestID, string jsonData)
        {
            try
            {
                DataContractJsonSerializer jss = new DataContractJsonSerializer(typeof(RawResponse<T>));
                MemoryStream ms = new MemoryStream(Encoding.Unicode.GetBytes(jsonData));

                RawResponse<T> rawResponse = jss.ReadObject(ms) as RawResponse<T>;

                if (rawResponse.error == null)
                    return new Response<T>(requestID, rawResponse.response, jsonData);
                else
                    return new Response<T>(requestID, rawResponse.error, jsonData);
            }
            catch (Exception ex)
            {
#if DEBUG
                Trace.WriteLine("Failed to get response!" + Environment.NewLine + ex.ToString()); 
#endif

                throw ex;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        private void DirectRequestHandler(Response<object> response)
        {
            if (this.DirectRequestCallback != null)
                this.DirectRequestCallback(response.ResponseID, response.RawJson);
        }

        /// <summary>
        /// 
        /// </summary>
        private long GetRequestID()
        {
            lock (this._syncRoot)
            {
                if (this._currentEventID > long.MaxValue - 10) // [Mudrov] I don't know why 10... just love this number
                    this._currentEventID = 0;

                this._currentEventID++;

                return this._currentEventID;
            }
        } 

        #endregion
    }
}
